Skip to main content

States and Effects

Resources

I. States

We often want our components to undergo visual changes as a result of user-computer interactions. To do this, a component needs to “remember” things about itself. This is what state is, a component’s memory.

1. useState hook

The useState [[03. States and Effects#II. Hooks |hook]] is a built-in hook in React that allows you to define state in a functional component. It takes an initial value as a parameter and returns an array with two elements that we can [[11. Objects#2. [Destructuring Assignment](https //developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) |destructure]] to get:

  1. The current state value
  2. A function to update the state value.
// Syntax 
const [stateValue, setStateValue] = useState(initialValue);

// Example with multiple state variables
const [count, setCount] = useState(0);
const [name, setName] = useState('');

When we call useState(), React automatically creates both the state variable and its corresponding setter function for us. We don't need to implement the setter function ourselves - React provides it ready to use.

const [backgroundColor, setBackgroundColor] = useState(COLORS[0]); // 
// ↑ state variable ↑ setter function ↑ initial value

2. React Events and Event Handling

In React, events are handled using camelCase naming (like onClick instead of onclick) and are passed as function references to JSX elements. When an event occurs, React automatically passes an event object to your event handler function.

event Object

  • The event object is automatically passed as the first parameter to your event handler functions
  • It's a cross-browser wrapper around the native browser event
  • It contains properties specific to the event type (e.g., event.target.value for input changes)
  • You don't need to explicitly declare it unless you need to use it
// The event object is implicitly passed to the handler
const handleClick = (event) => {
// 'event' contains information about the click
console.log('Button clicked at position:', event.clientX, event.clientY);

// event.target refers to the element that triggered the event
console.log('Button text:', event.target.textContent);
};

return <button onClick={handleClick}>Click me</button>;

Common event handlers in React include:

  • onClick: Triggered when an element is clicked
  • onChange: Triggered when an input element's value changes
  • onSubmit: Triggered when a form is submitted
  • onMouseOver/onMouseLeave: Triggered when the mouse enters/leaves an element
// Form input example
function NameInput() {
const [name, setName] = useState('');

const handleChange = (event) => {
// event.target.value contains the current input value
setName(event.target.value);
};

return <input value={name} onChange={handleChange} />;
}

3. State Updates and Rerendering

In React, when a component’s state or props change, the component is destroyed and recreated from scratch. This includes the variables, functions, and React nodes.

The entire component is recreated but this time the latest state value will be returned from useState. This process is called rerendering. Rerendering is a key feature of React that enables it to efficiently update the user interface in response to changes in the underlying data.

React’s State Management Lifecycle

Resource: Managing State

  1. Initial Render: React uses the initial value passed to useState
  2. State Update: When a setter function like setBackgroundColor is called, React:
    • Stores the new value internally
    • Schedules a rerender of the component
  3. Subsequent Renders: React ignores the initial value and uses the stored state value

React maintains this state in memory between renders, which creates the illusion that our component "remembers" information, even though the function itself is recreated from scratch.

Example 1: Implement parent-child component communication

  1. We define the state-changing function handleChangeMessage in the parent where the state lives
  2. We pass this function down to the child as a prop (named onMessageChange)
  3. The child component receives this prop through destructuring and attaches it to the button's onClick event

This pattern is fundamental to React's "uni-directional data flow" - flows down from parent to child via props, and changes flow back up through callback functions that were passed down.

// ParentComponent.jsx
import { useState } from 'react';
import ChildComponent from './ChildComponent';

function ParentComponent() {
const [message, setMessage] = useState("Hello World");

const handleChangeMessage = () => {
setMessage("Message Updated!");
};

return (
<div>
<h1>{message}</h1>
<ChildComponent onMessageChange={handleChangeMessage} />
</div>
);
}
// ChildComponent.jsx
function ChildComponent({ onMessageChange }) {
return (
<button onClick={onMessageChange}>Change Message</button>
);
}

**Example 2 **: An app where when a button is clicked, the entire background transforms to match the selected color.

Example CodeSandbox Demo

  • The backgroundColor state is defined with the useState hook. Then on every button, we set up a click event handler that calls the setBackgroundColor function with the corresponding value.
  • Whenever setBackgroundColor is called, our App component is rerendered. Essentially, the component is recreated and the backgroundColor is updated as well.
    • Which means the onButtonClick function and our div and buttons are recreated as well.
  • onButtonClick is an event handler, but with a specific pattern that makes it more flexible in React.
    • onButtonClick is what we call a "[[#Why Higher-Order Functions are Often Used?|higher-order function]]" because it returns another function that serves as the actual event handler.
    • It doesn’t return the result of calling setBackgroundColor(color) but returns a new function, an important distinction.
// Instead of doing this (which would call the function immediately): 
onClick={setBackgroundColor(color)} // ❌ This executes during render!

// We do this (which passes a function reference):
onClick={onButtonClick(color)} // ✅ This creates a function for later
import React, { useState } from "react";

const COLORS = ["pink", "green", "blue", "yellow", "purple"ư;

function App() {
// Initialize state with the first color from our COLORS array
// backgroundColor: current state value (e.g., "pink")
// setBackgroundColor: function to update the state
const [backgroundColor, setBackgroundColor] = useState(COLORS[0]);

// high-order event handler function that returns another function
const onButtonClick = (color) => {
// Return the "actual" event handler function
return () => {
setBackgroundColor(color); // this is where the background color is updated
};
};

return (
<div
className="App"
style={{ backgroundColor: backgroundColor }} // Apply the current background color
>
{/* Map over our colors array to create buttons */}
{COLORS.map((color) => (
<button
type="button"
key={color} // React needs a unique key for list items
onClick={onButtonClick(color)} // Attach our click handler
>
{color}
</button>
))}
</div>
);
}

export default App;

3. Higher-Order Functions vs. Direct/Inline Functions

We technically don’t need a onButtonClick function and instead utilize an inline arrow function for the onClick event.

{COLORS.map((color) => (
<button
type="button"
key={color}
onClick={() => setBackgroundColor(color)} // inline arrow function
className={backgroundColor === color ? "selected" : ""}
>
{color}
</button>
))}

However, the higher-order function pattern (const onButtonClick = (color) => () => {...}) is used for a few reasons:

  • Performance optimization: With inline arrow functions, a new function is created on every render. With the higher-order approach, the inner functions are created only once during initial render.
  • Code organization: It separates the creation of event handlers from their use in JSX, which can make the code more readable for complex components.
  • Event handler customization: It allows you to create specific handlers for different elements in a loop without defining multiple separate functions.

a. Direct Functions

  • The handler doesn't need any parameters beyond the event object
  • The action is consistent across all instances (like toggling a single state)
  • You're passing the handler to a single child component
  • The component isn't being re-rendered frequently
// For one-off interactions
// Direct function approach (declared separately)
const handleReset = () => {
setCount(0);
};

return <button onClick={handleReset}>Reset</button>;

// OR inline approach (same functionality)
return <button onClick={() => setCount(0)}>Reset</button>;

Note:

  • The onClick prop should receive a function reference, not a function call.
  • We write onClick={handleReset} (passing the function itself) rather than onClick={handleReset()} (immediately executing the function). → This way, React will only call the function when the click event occurs, not during rendering.

b. Higher-Order Functions

  • Each handler needs to "remember" specific data (like which item in a list it belongs to)
  • You're generating multiple similar handlers in a loop
  • You need to pass additional parameters beyond the event object
  • The component renders frequently and you want to optimize performance
// Higher-order function approach
const handleDelete = (id) => () => {
setItems(items.filter(item => item.id !== id));
};

return (
<ul>
{items.map(item => (
<li key={item.id}>
{item.name}
<button onClick={handleDelete(item.id)}>Delete</button>
</li>
))}
</ul>
);

4. Structuring States

Resources

a. States should not be mutated

Primitives are already immutable, but if you are using reference-type values, i.e., arrays and objects, never mutate them.

According to the React documentation, we should treat state as if it was immutable. To change state, we should always use the setState function.

Example:
function Person() {
const [person, setPerson] = useState({ name: "John", age: 100 });

// BAD - Don't do this!
const handleIncreaseAge = () => {
// mutating the current state object
person.age = person.age + 1;
setPerson(person);
};

// GOOD - Do this!
const handleIncreaseAge = () => {
// copy the existing person object into a new object
// while updating the age property
const newPerson = { ...person, age: person.age + 1 };
setPerson(newPerson);
};

return (
<>
<h1>{person.name}</h1>
<h2>{person.age}</h2>
<button onClick={handleIncreaseAge}>Increase age</button>
</>
);
}

If we don’t provide a new object to setState it is not guaranteed to re-render the page. This is because setState uses Object.is() to determine if the previous state is the same.

→ Therefore, we should always provide a new Object for setState to trigger a re-render. 

b. How states update

State updates are asynchronous. What this implies is that whenever you call the setState function, React will apply the update in the next component render.

Example:
  1. The component renders for the first time. The person state variable is initialized to { name: 'John', age: 100 }. The “during render” console.log prints the state variable.
  2. The button is clicked invoking handleIncreaseAge. Interestingly, the console.log before and after the setPerson call prints the same value.
  3. The component re-renders. The person state variable is updated to { name: 'John', age: 101 }.

→ The person state stays the same throughout the current render of the component. This is what “state as a snapshot” refers to. The setState call triggers a component re-render, and the person state is updated to the new value.

function Person() {
const [person, setPerson] = useState({ name: "John", age: 100 });

const handleIncreaseAge = () => {
console.log("in handleIncreaseAge (before setPerson call): ", person);
setPerson({ ...person, age: person.age + 1 });

// we've called setPerson, why isn't person updated?
console.log("in handleIncreaseAge (after setPerson call): ", person);
};

// this console.log runs every time the component renders
console.log("during render: ", person);

return (
<>
<h1>{person.name}</h1>
<h2>{person.age}</h2>
<button onClick={handleIncreaseAge}>Increase age</button>
</>
);
}

c. State Updater Function Pattern

When you pass in the value to the setState function, React will replace the current state with the value you passed in.

Example: When using direct state updates like this, the age will only increase by 1, not 2. This is because both updates are using the same snapshot of the person state from the current render.

const handleIncreaseAge = () => {
setPerson({ ...person, age: person.age + 1 });
setPerson({ ...person, age: person.age + 1 });
};

Only when using the updater function pattern can we truly update the state twice consecutively.

Example: When a callback is passed to the setState function, it ensures that the latest state is passed in as an argument to the callback.

const handleIncreaseAge = () => {
setPerson((prevPerson) => ({ ...prevPerson, age: prevPerson.age + 1 }));
setPerson((prevPerson) => ({ ...prevPerson, age: prevPerson.age + 1 }));
};

5. Controlled Components

There are native HTML elements that maintain their own internal state. The input element is a great example. You type into an input and it updates its own value for every keystroke. For many use cases, you would like to control the value of the input element, i.e., set its value yourself. This is where controlled components come in.

Example 1:
  • Instead of letting the input maintain its own state, we define our own state using the useState hook.
  • We then set the value prop of the input to the state variable and update the state variable on every onChange event.
  • Every keystroke triggers your onChange handler, which updates your state. React then updates the displayed value, giving you complete control over the input's behavior.
function CustomInput() {
const [value, setValue] = useState("");

return (
<input
type="text"
value={value}
onChange={(event) => setValue(event.target.value)}
/>
);
}
  • This pattern is handy wherever you need user input, i.e., typing in a textbox, toggling a checkbox, etc.
  • With an uncontrolled component, the browser's DOM manages the input value internally. You would typically access the value using a ref or when a form is submitted.

Example 2: Controlled Components with Parent-Child Communication

This example demonstrates a common pattern where the parent component maintains the state, while the child component handles the input display and user interactions:

  • The parent component owns and manages the state (text and setText)
  • The parent passes the current state value (currentText={text}) and an update function (onTextChange={updateText}) to the child
  • The child component renders the input and button UI elements
  • When user input occurs, the child calls the parent's update function
  • The parent updates its state, which then flows back down to the child as the new currentText value
// Parent Component
function ParentComponent() {
const [text, setText] = useState("Initial text");

const updateText = (newText) => {
setText(newText);
};

return (
<div>
<p>Text: {text}</p>
<ChildComponent currentText={text} onTextChange={updateText} />
</div>
);
}

// Child Component
function ChildComponent({ currentText, onTextChange }) {
// event handlers
const clearInput = () => {
onTextChange("");
};

const updateInput = (event) => {
onTextChange(event.target.value);
};

return (
<div>
<input type="text" value={currentText} onChange={updateInput} />
<button onClick={clearInput}>Clear Text</button>
</div>
);
}

II. Hooks

Hooks are functions that let you use React features. All hooks are recognizable by the use prefix. For example, useState is a hook.

  1. Hooks can only be called from the top level of a functional component.
  2. Hooks can’t be called from inside loops or conditions.

III. Side Effects

Resouces

A side-effect in React is when a component interacts with something outside itself - anything beyond the component's own props and state. These external interactions aren't part of React's rendering process.

Examples:
  • API calls: Fetching data from a server
  • DOM manipulation: Directly modifying HTML elements
  • Timer functions: Using setTimeout or setInterval
  • Browser storage: Working with localStorage or sessionStorage
  • Subscriptions: Setting up event listeners or WebSocket connections
  • Logging: Writing to the console or analytics services

In the example below, the component is reaching outside itself to get data from an API, which is a side-effect.

function UserProfile() {
// side effect - interacting with an external API
fetch('https://api.example.com/user/123')
.then(response => response.json())
.then(data => console.log(data));

return <div>User Profile</div>;
}

1. useEffect hook

The useEffect hook lets you perform side effects in function components. The hook consists of three key parts:

  • Effect function: The callback function containing the code to run
  • [[#a. Dependency Array|Dependency array]]: An optional array controlling when the effect runs
  • [[#b. The Cleanup Function|Cleanup function]]: An optional function returned by the effect
useEffect(() => {
// Side effect code here

return () => {
// return an optional cleanup code here
};
}, [/* optional dependency array */]);

a. Dependency Array

The dependency array controls when the useEffect hook runs.

  • By default, if no dependency array is specified, the useEffect hook runs on every render.
  • The second argument of useEffect accepts an array of dependencies allowing the hook to re-render only when those dependencies are changed.
// 1. useEffect runs after EVERY render - no dependency array specified

useEffect(() => {
console.log('Component updated');
});
// 2. useEffect runs ONLY on mount (first render) - empty dependency array specified

useEffect(() => {
console.log('Component mounted');
}, []);
// 3. useEffect runs on mount AND when any dependency changes - dependency array contains state variable

useEffect(() => {
console.log(`Count is now: ${count}`);
}, [count]);

b. The Cleanup Function

The useEffect can return a cleanup function which:

  • Runs before the effect runs again (if dependencies change)
  • Runs after the component unmounts It is used to prevents memory leaks and unexpected behavior and is crucial for subscriptions, timers, and event listeners.

c. Example - CodeSandBox

A Clock component that shows how many seconds have passed since the user has loaded the webpage.

Step 1: To update it every second, we can use setInterval method to add 1 to the counter state variable, every second.

import { useState } from "react";

export default function Clock() {
const [counter, setCounter] = useState(0);

setInterval(() => {
setCounter(count => count + 1)
}, 1000);

return (
<p>{counter} seconds have passed.</p>
);
}

→ The issue is, setInterval function is being called not once, but at every state render.

  • When the component first renders, it calls the initial setInterval function. Because the setInterval updates the state every second, it triggers the component to re-render to update the state of the count.
  • However, every re-render calls the setInterval function again, triggering more frequent state updates, spawning new intervals, making things go out of control.

Step 2: This issue can be partially solved with the useEffect hook along with the dependency array. We can wrap this calculation inside a useEffect to move it outside the rendering calculation, so that it’s only runs once on the first iteration.

We use an empty dependency array, so that the useEffect hook only runs once on mount, hence calling the setInterval method only once.

function Clock() {
const [counter, setCounter] = useState(0);

useEffect(() => {
// This interval will be set up when the component mounts
setInterval(() => {
setCounter(count => count + 1);
}, 1000);
}, []); // empty dependency array means useEffect "run once on mount"

return <p>{counter} seconds have passed.</p>;
}

→ However, still have an issue with the counter updating twice every second.

Step 3: To fix the counter updating issue and clean up the interval when the component unmount, we can utilize the cleanup function.

import { useEffect, useState } from "react";

export default function Clock() {
const [counter, setCounter] = useState(0);

useEffect(() => {
// Store the interval ID so we can clear it later
const intervalId = setInterval(() => {
setCounter(count => count + 1)
}, 1000);

// Return cleanup function that runs when component unmounts
// or before the effect runs again
return () => {
clearInterval(intervalId);
};
}, []); // Empty array means "only run on mount"

return (
<p>{counter} seconds have passed.</p>
);
}

2. Using Effects

useEffect is a mechanism outside the concepts that React usually applies, allowing you to sync your component with various external systems like a server, API, or browser DOM.

→ However, unnecessary useEffect hooks are error-prone and cause unnecessary performance issues. Before using an effect, it’s important to know if such external systems exist and need to be synced with, apart from props or state.

The primary purpose of useEFfect is to sync your component with external systems like APIs, browser DOM, or third-party libraries, etc.

1. When to Use useEffect

a. Connecting to a WebSocket
useEffect(() => {
const socket = new WebSocket('wss://example.com');

socket.onmessage = (event) => {
setMessages(prev => [...prev, event.data]);
};

// Cleanup: close connection when component unmounts
return () => {
socket.close();
};
}, []); // Empty dependency array means this runs once on mount
b. Fetching Data from an API
useEffect(() => {
let isMounted = true;

const fetchData = async () => {
try {
const response = await fetch(`https://api.example.com/user/${userId}`);
const data = await response.json();

if (isMounted) {
setUserData(data);
}
} catch (error) {
if (isMounted) {
setError(error);
}
}
};

fetchData();

// Cleanup function to prevent state updates if component unmounts during fetch
return () => {
isMounted = false;
};
}, [userId]); // Re-run when userId changes
c. Setting up and Cleaning up DOM Events outside React’s control
useEffect(() => {
const handleResize = () => {
setWindowWidth(window.innerWidth);
};

window.addEventListener('resize', handleResize);

// Cleanup: remove event listener when component unmounts
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // Empty array means this runs once on mount
d. Working with Third-Party libraries
useEffect(() => {
const chart = new ChartLibrary('#chart-container', {
data: chartData,
options: chartOptions
});

// Cleanup: destroy chart instance when component unmounts
return () => {
chart.destroy();
};
}, [chartData]); // Re-run when chartData changes
e. Timers and Intervals
useEffect(() => {
const timer = setInterval(() => {
setCounter(count => count + 1);
}, 1000);

// Cleanup: clear the interval when component unmounts
return () => {
clearInterval(timer);
};
}, []); // Empty array means this runs once on mount

2. When NOT to Use useEffect

a. Calculations During Rendering

If you're only calculating something based on existing state, do it directly in your render function instead of using an effect.

// ❌ Don't do this
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');

useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);

// ✅ Do this instead
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const fullName = `${firstName} ${lastName}`;
b. Event Handling

Code that should run in response to specific user interactions should be in even handlers, not effects.

// ❌ Don't do this
useEffect(() => {
if (buttonClicked) {
performAction();
setButtonClicked(false);
}
}, [buttonClicked]);

// ✅ Do this instead
const handleButtonClick = () => {
performAction();
};

return <button onClick={handleButtonClick}>Click Me</button>;
c. Managing Child Component State
// ❌ Don't do this
function ParentComponent() {
const [parentState, setParentState] = useState(0);

return <ChildComponent onUpdate={setParentState} />;
}

function ChildComponent({ onUpdate }) {
const [childState, setChildState] = useState(0);

useEffect(() => {
onUpdate(childState);
}, [childState, onUpdate]);

// ...
}

// ✅ Do this instead (lift state up)
function ParentComponent() {
const [sharedState, setSharedState] = useState(0);

return <ChildComponent value={sharedState} onChange={setSharedState} />;
}

function ChildComponent({ value, onChange }) {
// Use the shared state directly
// ...
}

3. Additional Notes - Using useEffect

1. Include a Dependency Array

// ❌ No dependency array (runs after every render)
useEffect(() => {
document.title = `You clicked ${count} times`;
});

// ✅ With dependency array (runs only when dependencies change)
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]);

2. Separate Effects for Different Concerns

It's better to separate unrelated logic into different effects.

// ✅ Separate effects for separate concerns
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]);

useEffect(() => {
const handleKeyPress = (e) => {
if (e.key === 'Escape') {
handleClose();
}
};

window.addEventListener('keydown', handleKeyPress);
return () => {
window.removeEventListener('keydown', handleKeyPress);
};
}, [handleClose]);

3. Cleanup Functions

Always clean up any subscriptions, timers, or event listeners in the effect's return function.

useEffect(() => {
const subscription = subscribeToData(dataId);

// Return a cleanup function
return () => {
subscription.unsubscribe();
};
}, [dataId]);